「同一個 PurchaseRequest,要同時支援信用卡、ATM、超商代碼、無卡分期、電子發票,這麼多種欄位組合,是不是要寫一堆 if 判斷付款方式,然後每種都塞一大包參數?」
omnipay-ecpay 這個套件的做法不是這樣。它把「一種付款方式需要哪些欄位」拆成一個個 Trait,再讓 PurchaseRequest 這個類別 use 它需要的那幾個,最後用一個方法把它們依「使用者選的付款方式」動態組裝起來。今天就把這個設計攤開來看。
if
getSendExtend() 這個組裝方法怎麼依 ChoosePayment 決定要不要帶某組欄位src/Traits/ 底下有 14 個檔案,光看名字就能猜到分工:
HasATMFields.php ATM 專屬欄位
HasATMOrCVSOrBARCODEFields.php ATM/超商代碼/條碼共用欄位
HasCreditFields.php 信用卡分期、定期定額、銀聯卡等欄位
HasCVSOrBARCODEFields.php 超商代碼/條碼共用欄位
HasInvoiceFields.php 電子發票欄位
HasECPay.php 包裝官方 SDK 的共用邏輯(明天會細講)
...(其餘 8 個負責金額、預設值、商店代號等更基礎的欄位)
例如 HasATMFields 只負責一件事——ATM 繳費期限:
trait HasATMFields
{
public function setExpireDate($value)
{
return $this->setParameter('ExpireDate', $value);
}
public function getExpireDate()
{
return $this->getParameter('ExpireDate') ?: 3;
}
}
HasCreditFields 則負責信用卡相關的一大票欄位(分期期數、定期定額金額/週期、記憶卡號、銀聯卡選項……),每個 setter/getter 都配了中文註解說明綠界要求的參數規則。
PurchaseRequest 怎麼把這些 Trait 組回來class PurchaseRequest extends AbstractRequest
{
use HasAmount;
use HasATMFields;
use HasATMOrCVSOrBARCODEFields;
use HasCreditFields;
use HasCustomFields;
use HasCVSOrBARCODEFields;
use HasDefaults;
use HasInvoiceFields;
use HasMerchantTradeNo;
use HasSendFields;
use HasStoreID;
// ...
}
一個類別 use 了 11 個 Trait,這代表 PurchaseRequest 的實例上同時擁有所有付款方式的 setter/getter(setCreditInstallment()、setExpireDate()、setCustomerIdentifier() 全部都在)。但真正要送給綠界的資料,並不是把所有欄位全部塞進去——而是靠 getSendExtend() 依使用者選的 ChoosePayment 動態決定:
private function getSendExtend($sendFields)
{
return array_merge(
$this->getCreditFields($sendFields['ChoosePayment']),
$this->getATMFields($sendFields['ChoosePayment']),
$this->getCvsFields($sendFields['ChoosePayment']),
$this->getBNPLFields($sendFields['ChoosePayment']),
$this->getInvoiceFields($sendFields['InvoiceMark'])
);
}
private function getATMFields($choosePayment)
{
return in_array($choosePayment, ['ALL', 'ATM'], true) ? [
'ExpireDate' => $this->getExpireDate(),
'PaymentInfoURL' => $this->getPaymentInfoURL(),
'ClientRedirectURL' => $this->getClientRedirectURL(),
] : [];
}
每個 getXxxFields() 私有方法只做一件事:判斷這次付款方式用不用得到這組欄位,用得到就回傳陣列,用不到就回傳空陣列,最後全部 array_merge 起來。
❌ 反例:所有欄位判斷擠在一個方法裡
private function getSendExtend($sendFields)
{
$extend = [];
$payment = $sendFields['ChoosePayment'];
if ($payment === 'ALL' || $payment === 'Credit') {
$extend['CreditInstallment'] = $this->getCreditInstallment();
$extend['InstallmentAmount'] = $this->getInstallmentAmount();
$extend['Redeem'] = $this->getRedeem();
// ...還有將近 10 個信用卡欄位
}
if ($payment === 'ALL' || $payment === 'ATM') {
$extend['ExpireDate'] = $this->getExpireDate();
$extend['PaymentInfoURL'] = $this->getPaymentInfoURL();
// ...
}
// ATM、CVS、BNPL、Invoice 全部混在同一個方法、同一層縮排
return $extend;
}
✅ 正例:每種付款方式各自一個小方法,用組合的方式接起來
private function getSendExtend($sendFields)
{
return array_merge(
$this->getCreditFields($sendFields['ChoosePayment']),
$this->getATMFields($sendFields['ChoosePayment']),
$this->getCvsFields($sendFields['ChoosePayment']),
$this->getBNPLFields($sendFields['ChoosePayment']),
$this->getInvoiceFields($sendFields['InvoiceMark'])
);
}
正例的好處不是程式碼變短,而是每個付款方式的欄位規則可以被獨立測試、獨立修改。這也解釋了為什麼 tests/Message/PurchaseRequestTest.php 可以針對 Credit、ATM、BNPL、彈性分期各寫一個獨立測試——測試結構直接對應著程式碼的拆分方式。程式碼怎麼拆,測試就會怎麼長;拆得好,測試自然就好寫。
Trait 組合不是沒有代價。一個 PurchaseRequest 實例身上掛了 11 個 Trait 的方法,你光看類別簽章看不出它實際支援哪些付款方式的欄位——要嘛去翻 use 的清單,要嘉去翻 getSendExtend()。這也呼應昨天提到的:README 沒說清楚的東西,最後只能靠翻程式碼或翻測試補回來。
如果只有 2、3 種付款方式,這種拆法可能反而是過度設計;但當付款方式一路長到 5 種以上、欄位規則彼此獨立又各自複雜(像信用卡那組近 10 個參數),拆開來讓每組欄位各自可測試,會比一個愈長愈難讀的大方法更容易維護。該不該拆,要看的是「欄位規則彼此獨立的程度」,不是「類別數量看起來多不多」。
回頭看看你手上維護的專案,有沒有一個方法因為「要處理好幾種模式/類型」而越長越難讀?如果把它拆成 Trait 或小方法組合起來,你覺得會變得更好測,還是只是把複雜度搬到別的地方?
omnipay-ecpay 用 14 個 Trait 分別封裝各付款方式的欄位,PurchaseRequest 全部 use 進來getSendExtend() 依 ChoosePayment 動態決定要合併哪些欄位,每種付款方式各自一個私有方法明天會看 HasECPay 這個 Trait,它包的不是欄位,而是綠界官方 SDK 本身——一個要給多人共用的套件,要怎麼把廠商自己發布的 SDK 介面,接進 Omnipay 統一的 Gateway/Request/Response 介面裡。